#include "hw_shadowmap_demo.h"
#include "geometry/matrix4x4.h"
#include "graphics/utils.h"
#include "graphics/image.h"
#include "glh/glh_extensions.h"
#include "glh/glh_obs.h"
#include "glh/glh_glut.h"
#include "graphics/image_texture_gl.h"
#include "graphics/framebuffer_object.h"

namespace venus {
using namespace glh;
void SetTexGenPlanes(GLenum plane_type, const Matrix4x4 &m) {
  GLenum coord[] = {GL_S, GL_T, GL_R, GL_Q };
  for(int i = 0; i < 4; i++) {
    glTexGenfv(coord[i], plane_type, m[i]);
  }
}
void EyeLinearTexGen() {
  Matrix4x4 m;
  SetTexGenPlanes(GL_EYE_PLANE, m);
  glTexGeni(GL_S, GL_TEXTURE_GEN_MODE, GL_EYE_LINEAR);
  glTexGeni(GL_T, GL_TEXTURE_GEN_MODE, GL_EYE_LINEAR);
  glTexGeni(GL_R, GL_TEXTURE_GEN_MODE, GL_EYE_LINEAR);
  glTexGeni(GL_Q, GL_TEXTURE_GEN_MODE, GL_EYE_LINEAR);
}
void ObjLinearTexGen() {
  Matrix4x4 m;
  SetTexGenPlanes(GL_OBJECT_PLANE, m);
  glTexGeni(GL_S, GL_TEXTURE_GEN_MODE, GL_OBJECT_LINEAR);
  glTexGeni(GL_T, GL_TEXTURE_GEN_MODE, GL_OBJECT_LINEAR);
  glTexGeni(GL_R, GL_TEXTURE_GEN_MODE, GL_OBJECT_LINEAR);
  glTexGeni(GL_Q, GL_TEXTURE_GEN_MODE, GL_OBJECT_LINEAR);
}

void EnableTexGen() {
  glEnable(GL_TEXTURE_GEN_S);
  glEnable(GL_TEXTURE_GEN_T);
  glEnable(GL_TEXTURE_GEN_R);
  glEnable(GL_TEXTURE_GEN_Q);
}
void DisableTexGen() {
  glDisable(GL_TEXTURE_GEN_S);
  glDisable(GL_TEXTURE_GEN_T);
  glDisable(GL_TEXTURE_GEN_R);
  glDisable(GL_TEXTURE_GEN_Q);
}


bool HwShadowMapDemo::Initialize() {
  glewInit();
  if (!glewIsSupported( "GL_VERSION_2_0 GL_EXT_framebuffer_object")) {
    printf( "Required extensions not supported.\n");
    return false;
  }
  glClearColor(.5f, .5f, .5f, .5f);
  Image img;
  V_RET(img.Load("decal_image.png"));
  decal_tex_ = new ImageTextureGL;
  V_RET(decal_tex_->Initialize(img, GL_RGB, kUseMipmaps));
  decal_tex_->SetParameter(GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
  decal_tex_->SetParameter(GL_TEXTURE_MAG_FILTER, GL_LINEAR);
  V_RET(img.Load("nvlogo_spot.png"));
  light_tex_ = new ImageTextureGL;
  V_RET(light_tex_->Initialize(img, GL_RGB));

  V_RET(this->CreateObjects());

  glEnable(GL_LIGHT0);
  float black[4] = { 0.0, 0.0, 0.0, 0.0 };
  float light_intensity[4] = { 1, 1, 1, 1 };
  glLightfv(GL_LIGHT0, GL_AMBIENT, black);
  glLightfv(GL_LIGHT0, GL_DIFFUSE, light_intensity);
  glLightfv(GL_LIGHT0, GL_SPECULAR, light_intensity);

  glEnable(GL_DEPTH_TEST);
  shadow_map_ = new ImageTextureGL;
  V_RET(shadow_map_->Initialize(shadow_map_width_, shadow_map_height_,
                                GL_DEPTH_COMPONENT, 
                                kUseDefaultParameters|kDepthTexture));
  shadow_map_->Bind();
  shadow_map_->SetParameter(GL_TEXTURE_COMPARE_MODE_ARB,
                            GL_COMPARE_R_TO_TEXTURE_ARB);
  shadow_map_->SetParameter(GL_TEXTURE_COMPARE_FUNC_ARB, GL_LEQUAL);

  fbo_ = new FramebufferObject;
  fbo_->Bind();
  fbo_->AttachTexture(shadow_map_->target(), 
                      shadow_map_->tex_id(), GL_DEPTH_ATTACHMENT_EXT);
  glDrawBuffer(GL_NONE);
  glReadBuffer(GL_NONE);
  if (!fbo_->IsValid()) return false;
  fbo_->Disable();

  object.dolly.dolly[2] = -3; // push plane forward
  object.trackball.r = rotationf(vec3f(1,0,0), to_radians(30));
  spotlight.pan.pan[0] = -.25;
  spotlight.pan.pan[1] = 1.5;
  spotlight.dolly.dolly[2] = -2.; // push spotlight forward
  spotlight.trackball.r =  rotationf(vec3f(1.f, 0.f, 0.f), to_radians(-60.f));

  camera.set_camera_mode(true);
  spotlight.set_camera_mode(false);
  spotlight.trackball.invert_increment = true;

  camera.set_parent_rotation( & camera.trackball.r);
  object.set_parent_rotation( & camera.trackball.r);
  spotlight.set_parent_rotation( & camera.trackball.r);

  lightshaper.fovy  = 60.f;
  lightshaper.zNear = .5f;
  lightshaper.zFar  = 5.f;
  lightshaper.width = shadow_map_width_;
  lightshaper.height = shadow_map_height_;


  camera.disable();
  spotlight.disable();
  lightshaper.disable();
  // attach interactors to the event multiplexor
  glut_add_interactor(& reshaper);
  glut_add_interactor(& object);
  glut_add_interactor(& camera);
  glut_add_interactor(& spotlight);

  render_method_ = 1;
  float bias = 1/(pow(2.0,16.0)-1);

  tweak t;

  t.name = "r coordinate scale";
  t.val = .5;
  t.incr = bias;
  tweaks[t.name] = t;

  t.name = "r coordinate bias";
  t.val = .5;
  t.incr = bias;
  tweaks[t.name] = t;

  t.name = "polygon offset scale";
  t.val = 2.5;
  t.incr = .5;
  tweaks[t.name] = t;

  t.name = "polygon offset bias";
  t.val = 10;
  t.incr = 1;
  tweaks[t.name] = t;

  curr_tweak = tweaks.begin();
  return true;
}

bool HwShadowMapDemo::CreateObjects() {
  quad_ = glGenLists(1);
  if (!quad_) {
    Error("create quad failed!");
    return false;
  }
  glNewList(quad_, GL_COMPILE);
  glPushMatrix();
  glRotatef(-90, 1, 0, 0);
  glScalef(4,4,4);
  glBegin(GL_QUADS);
  glNormal3f(0, 0, 1);
  glVertex2f(-1, -1);
  glVertex2f(-1,  1);
  glVertex2f( 1,  1);
  glVertex2f( 1, -1);
  glEnd();
  glPopMatrix();
  glEndList();

  wire_cube_ = glGenLists(1);
  if (!wire_cube_) {
    Error("create wire cube failed!");
    return false;
  }
  glNewList(wire_cube_, GL_COMPILE);
  glutWireCube(2);
  glEndList();

  teapot_ = glGenLists(1);
  if (!teapot_) {
    Error("create teapot failed!");
    return false;
  }
  glNewList(teapot_, GL_COMPILE);
  glPushMatrix();
  glTranslatef(0, 0.4f, 0);
  glutSolidTeapot(0.5f);
  glPopMatrix();
  glEndList();
  return true;
}

void HwShadowMapDemo::LargestSquarePowerOfTwoViewport() {
  GLfloat min = reshaper.width < reshaper.height ? 
      reshaper.width : reshaper.height;
  GLfloat log2min = log(min) / log(2.0);
  GLfloat pow2 = floor(log2min);
  GLuint size = 1 << static_cast<int>(pow2);
  //glViewport(0,0, size, size);
  glViewport(0,0, min, min);
}

void HwShadowMapDemo::OnDisplay() {
  glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
  if (render_method_ == 1) RenderSceneFromeCameraViewWithShadow();
  else if (render_method_ == 2) RenderSceneFromeCameraView();
  else if (render_method_ == 3) RenderSceneFromLightView();
  else VisualizeTexture(*shadow_map_);
  DisplayStastics("frame rate", fps_counter_.Update(), "fps");
  glutSwapBuffers();
}

void HwShadowMapDemo::RenderSceneFromLightView() {
  // place light
  glPushMatrix();
  glLoadIdentity();
  glLightfv(GL_LIGHT0, GL_POSITION, light_.pos.ptr());
  glPopMatrix();

  // spot image
  glActiveTextureARB(GL_TEXTURE1_ARB);

  glPushMatrix();
  EyeLinearTexGen();
  EnableTexGen();
  glPopMatrix();

  glMatrixMode(GL_TEXTURE);
  glLoadIdentity();
  glTranslatef(.5f, .5f, .5f);
  glScalef(.5f, .5f, .5f);
  gluPerspective(lightshaper.fovy, 1, lightshaper.zNear, lightshaper.zFar);
  glMatrixMode(GL_MODELVIEW);

  light_tex_->Bind();
  light_tex_->Enable();
  glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE);

  //glActiveTextureARB(GL_TEXTURE0_ARB);

  // the view frustum of light is square, so the viewport also need to be set
  // as square, which is done by Largest...()
  lightshaper.apply(); 
  // when render shadow map, it needs to render from light's view,
  // if the Largest...() is called, then the viewport conflicts with that
  // set in rendering shadow map, and will lead to error!
  if (render_method_ == 3) LargestSquarePowerOfTwoViewport();
  RenderScene(spotlight);

  glActiveTextureARB(GL_TEXTURE1_ARB);
  light_tex_->Disable();
  glActiveTextureARB(GL_TEXTURE0_ARB);
}

void HwShadowMapDemo::RenderScene( glh::glut_simple_mouse_interactor &view ) {
  glColor3f(1,1,1);
  glPushMatrix();
  view.apply_inverse_transform();

  glPushMatrix();
  object.apply_transform();

  glActiveTextureARB(GL_TEXTURE0_ARB);
  ObjLinearTexGen();
  EnableTexGen();
  glMatrixMode(GL_TEXTURE);
  glLoadIdentity();
  glScalef(4, 4, 1); // this can affect the definition of the decal texture
  glMatrixMode(GL_MODELVIEW);

  glDisable(GL_LIGHTING);
  decal_tex_->Bind();
  decal_tex_->Enable();
  glCallList(quad_);
  decal_tex_->Disable();
  decal_tex_->Unbind();

  glEnable(GL_LIGHTING);
  DisableTexGen();
  glMatrixMode(GL_TEXTURE);
  glLoadIdentity();
  glMatrixMode(GL_MODELVIEW);

  //maybe the light texture is used when lighting is enabled
  glEnable(GL_LIGHTING);
  glCallList(teapot_);
  glDisable(GL_LIGHTING);
  glPopMatrix();
  glPopMatrix();
}

void HwShadowMapDemo::OnResize(int width, int height) {
  //reshaper.reshape(width, height);
}
void HwShadowMapDemo::OnIdle() {
  this->ReDraw();
}
void HwShadowMapDemo::OnKeyboard(unsigned char key, int x, int y) {
  switch (key) {
  case '1' : 
    render_method_ = 1;
    break;
  case '2':
    render_method_ = 2;
    break;
  case '3':
    render_method_ = 3;
    break;
  case '4':
    render_method_ = 4;
    break;
  default:
    break;
  }
}

void HwShadowMapDemo::RenderSceneFromeCameraView() {
  // place light
  glPushMatrix();
  glLoadIdentity();
  camera.apply_inverse_transform();
  spotlight.apply_transform();
  glLightfv(GL_LIGHT0, GL_POSITION, & vec4f(0,0,0,1)[0]);
  glPopMatrix();

  // spot image
  glActiveTextureARB(GL_TEXTURE1_ARB);

  glPushMatrix();
  camera.apply_inverse_transform();
  EyeLinearTexGen();    
  EnableTexGen();
  glPopMatrix();

  glMatrixMode(GL_TEXTURE);
  glLoadIdentity();
  glTranslatef(.5f, .5f, .5f);
  glScalef(.5f, .5f, .5f);
  gluPerspective(lightshaper.fovy, 1, lightshaper.zNear, lightshaper.zFar);
  spotlight.apply_inverse_transform();
  glMatrixMode(GL_MODELVIEW);

  light_tex_->Bind();
  light_tex_->Enable();
  glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE);

  glActiveTextureARB(GL_TEXTURE0_ARB);
  reshaper.apply();
  RenderScene(camera);

  glActiveTextureARB(GL_TEXTURE1_ARB);
  light_tex_->Disable();
  glActiveTextureARB(GL_TEXTURE0_ARB);
}

void HwShadowMapDemo::RenderSceneFromeCameraViewWithShadow()
{
  this->UpdateShadowMap();
  // place light
  glPushMatrix();
  glLoadIdentity();
  camera.apply_inverse_transform();
  spotlight.apply_transform();
  glLightfv(GL_LIGHT0, GL_POSITION, & vec4f(0,0,0,1)[0]);
  glPopMatrix();

  glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

  // spot image
  glActiveTextureARB(GL_TEXTURE1_ARB);

  glPushMatrix();
  camera.apply_inverse_transform();
  EyeLinearTexGen();
  EnableTexGen();
  glPopMatrix();

  glMatrixMode(GL_TEXTURE);
  glLoadIdentity();
  glTranslatef(.5f, .5f, .5f);
  glScalef(.5f, .5f, .5f);
  gluPerspective(lightshaper.fovy, 1, lightshaper.zNear, lightshaper.zFar);
  spotlight.apply_inverse_transform();
  glMatrixMode(GL_MODELVIEW);

  light_tex_->Bind();
  light_tex_->Enable();
  glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE);

  // depth compare
  glActiveTextureARB(GL_TEXTURE2_ARB);

  glPushMatrix();
  camera.apply_inverse_transform();
  EyeLinearTexGen();
  EnableTexGen();
  glPopMatrix();

  glMatrixMode(GL_TEXTURE);
  glLoadIdentity();
  glTranslatef(.5f, .5f, tweaks["r coordinate scale"].val);
  glScalef(.5f, .5f, tweaks["r coordinate bias"].val);
  gluPerspective(lightshaper.fovy, 1, lightshaper.zNear, lightshaper.zFar);
  spotlight.apply_inverse_transform();
  glMatrixMode(GL_MODELVIEW);

  shadow_map_->Bind();
  shadow_map_->Enable();
  glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE);

  glActiveTextureARB(GL_TEXTURE0_ARB);

  reshaper.apply();
  RenderScene(camera);

  glActiveTextureARB(GL_TEXTURE1_ARB);
  light_tex_->Disable();
  glActiveTextureARB(GL_TEXTURE2_ARB);
  shadow_map_->Disable();
  glActiveTextureARB(GL_TEXTURE0_ARB);
}

void HwShadowMapDemo::UpdateShadowMap() {
  int viewport[4];
  glGetIntegerv(GL_VIEWPORT, viewport);
  //if(light_view_version == master_light_view_version) return;

  fbo_->Bind();
  glViewport(0, 0, shadow_map_width_, shadow_map_height_);
  glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
  glPolygonOffset(tweaks["polygon offset scale"].val, 
                  tweaks["polygon offset bias"].val);
  glEnable(GL_POLYGON_OFFSET_FILL);
  this->RenderSceneFromLightView();
  glDisable(GL_POLYGON_OFFSET_FILL);
  fbo_->Disable();
  glViewport(viewport[0], viewport[1], viewport[2], viewport[3]);
}

HwShadowMapDemo::~HwShadowMapDemo() {
  Utils::Delete(fbo_);
  Utils::Delete(decal_tex_);
  Utils::Delete(shadow_map_);
  Utils::Delete(light_tex_);
}

} // namespace venus
